#pragma once
#ifndef _KH_ENGINE_LEVEL_H
#define _KH_ENGINE_LEVEL_H
#include <Kharlia/Engine/Common.h>
#include <Kharlia/Engine/Drawable.h>
#include <Kharlia/Engine/Units.h>
#include <Kharlia/Engine/TileData.h>
#include <vector>
#include <iostream>
#include <map>

#define COORDS_TO_TILE(tx, ty) (((tx % 16) + ((tx / 16) * 512)) + ((ty * 16) % 512))
#define TILE_TO_X(tilecode) ((tilecode % 16) + ((tilecode / 512) * 16))
#define TILE_TO_Y(tilecode) ((tilecode / 16) % 32)

namespace kharlia { namespace core {

class CTasker;

}}

namespace kharlia { namespace engine {

class FTexture;

class FTileLayer;
class CGame;
class CWorld;
class CPlayer;

class CActor;
class ACollider;
class ARender;
class AMovement;
class APlayerController;

namespace ENetRole { enum Type; }

// Tile coordinate encoding and decoding functions
FORCEINLINE
tile_t TileFromCoords(int tx, int ty) {
    return tile_t(((tx % 16) + ((tx / 16) * 512)) + ((ty * 16) % 512));
}

FORCEINLINE
int TileToX(tile_t code) {
    return int((code % 16) + ((code / 512) * 16));
}

FORCEINLINE
int TileToY(tile_t code) {
    return int((code / 16) % 32);
}

FORCEINLINE
tile_t TileFromVector2(FVector2I const& v) {
    return TileFromCoords(v.X, v.Y);
}

FORCEINLINE
FVector2I TileToVector2(tile_t code) {
    return FVector2I(TileToX(code),
                    TileToY(code));
}

class KH_ENGINE_API FTileLayer: public FDrawable {
    DECL_PICKLER_NOSTATE_N(FTileLayer);
    shared_ptr<FTileData> mTileData;

public:
    FTileLayer();
    FTileLayer(FSizeI const& size);
    FTileLayer(shared_ptr<FTileData> tile_data);
    virtual ~FTileLayer();

    bool GetModified() { return GetRenderInfoInvalid(); }
    void SetModified(bool value) { SetRenderInfoInvalid(value); }

    shared_ptr<FTileData> const& GetTileData() { return mTileData; }

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
    friend class CLevel;
};

class KH_ENGINE_API FTileGroup {
public:
    typedef std::vector<shared_ptr<FTileLayer>> LayerList;

private:
    DECL_PICKLER_N(FTileGroup, tuple);
    FSizeI mSize;
    LayerList mLayers;

public:
    FTileGroup(FSizeI const& size);
    virtual ~FTileGroup() {}

    void AddLayer(int index);
    void AddLayer(int index, shared_ptr<FTileLayer> tile_layer);

    FSizeI const& GetSize() const { return mSize; }
    int GetLayerCount() const { return mLayers.size(); }
    shared_ptr<FTileLayer> const& GetLayer(int index);

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
    friend void _ExitLevelClasses();
    friend class CLevel;
    friend struct PSFTileGroup;

    static const uint8 WIDTH = 64;
    static const uint8 HEIGHT = 64;
};

struct FTileInfo {
    uint8 Blocking: 1;
    uint8 Shape: 7;
    // Can't get pointer to bit field, make wrappers for python
    uint8 GetBlocking() const { return Blocking; }
    void SetBlocking(uint8 value) { Blocking = value; }
    uint8 GetShape() const { return Shape; }
    void SetShape(uint8 value) { Shape = value; }

    operator uint8() const { return *((uint8*)this); }
};

static_assert(sizeof(FTileInfo) == sizeof(uint8), "Tile info must be a single byte");

//struct FTileDef {
//    FRectI Rect;
//    shared_ptr<FTexture> Texture;
//    std::vector<FTileInfo> TileInfo;
//};

class KH_ENGINE_API FTileset: public FDrawable {
public:
    // Page size in tiles
    static const uint8 PAGE_WIDTH = 16;
    static const uint8 PAGE_HEIGHT = 32;
    static const uint16 PAGE_SIZE = PAGE_WIDTH * PAGE_HEIGHT;

private:
    DECL_PICKLER_N(FTileset, tuple);

    str mName;
    shared_ptr<FTexture> mTexture;
    uint16 mSize; // Size in tile pages (16x32 tiles)
    uint16 mOffset; // Offset in pages of these tile codes.
    std::vector<FTileInfo> mTileInfo;
    //std::vector<FTileDef> mTileDefs;

public:

    FTileset();
    FTileset(str name, uint16 size, uint16 offset=0);
    
    bool CheckBlocking(uint16 code) const {
        return mTileInfo[code - (PAGE_SIZE * mOffset)].Blocking;
    }
    FTileInfo const& GetTileInfoByIndex(int index) const {
        return mTileInfo.at(index);
    }
    FTileInfo const& GetTileInfoByCode(uint16 code) const {
        return mTileInfo.at(code - (PAGE_SIZE * mOffset));
    }
    int GetTileCount() const {
        return PAGE_SIZE * mSize;
    }
    void UpdateTexture() {
        SetRenderInfoInvalid(true);
    }

    str const& GetName() const { return mName; }
    shared_ptr<FTexture> const& GetTexture() const { return mTexture; }
    void SetTexture(shared_ptr<FTexture> const& v) { mTexture = v; }
    uint16 GetSize() const { return mSize; }
    void SetSize(uint16 newsize);
    uint16 GetOffset() const { return mOffset; }
    void SetOffset(uint16 value) { mOffset = value; }
    std::vector<FTileInfo> const& GetTileInfo() const { return mTileInfo; }
    //std::vector<FTileDef> const& GetTileDefs() const { return mTileDefs; }

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
};

struct KH_ENGINE_API FLevelLayer {
    DECL_PICKLER_N(FLevelLayer, object);

    int Index;
    unicode Name;
    union {
        struct {
            uint8 Red;
            uint8 Green;
            uint8 Blue;
            uint8 Alpha;
        };
        uint32 Color;
    };
    bool NoCollision;
    bool Visible;
    shared_ptr<FTileset> Tileset;

    FLevelLayer():
        Index(0),
        Name(),
        Red(255),
        Green(255),
        Blue(255),
        Alpha(255),
        NoCollision(false),
        Visible(true) {}
};

class KH_ENGINE_API CLevel: public core::CObject {
public:
    typedef std::vector<shared_ptr<FLevelLayer>> LayerList;
    typedef std::vector<TPtr<CActor>> ActorList;
    typedef std::vector<TPtr<CActor>> RenderableList;
    typedef std::vector<shared_ptr<FTileGroup>> TileGroupList;

private:
    DECL_PICKLER_NOINIT_N(CLevel, tuple);
    /// The world that contains this level
    TWeakPtr<CWorld> mWorld;
    /// Unicode name of the level
    unicode mName;
    /// Hash of the level name, a Python int
    object mNameHash;
    /// GUID of the level.
    /// The level's name is not sufficiently unique for identification
    object mGUID;
    /// Logger object
    shared_ptr<core::FLogger> mLogger;
    /// Counter used for generating actor IDs.
    uint32 mIDCounter;
    /// Level size in tile groups
    FSizeI mSize;
    
    FSizeI mTileGroupSize;
    /// The level's tile groups
    std::vector<shared_ptr<FTileGroup>> mTileGroups;
    /// Layer definitions
    LayerList mLayers;
    /// Actors
    ActorList mActors;
    dict mActorMap; // name -> actor
    dict mActorIDMap; // id -> actor
    /// True if the game has been started
    bool mGameStarted;
    RenderableList mRenderables;
    handle<PyChannelObject> mTickChannel;
    // Physics
    std::vector<TPtr<ACollider>> mCollidables;
    sint16 mCollisionLayer;

    static object msPlayerControllerClass;
    static object msActorClass;

    void Finalize();
    //void InitCollidable(FLevelCollidable&);
    //void CleanupCollidable(FLevelCollidable&);
    void AddActor(TPtr<CActor> actor, uint32 id=0, object name=object());
    void RemoveActor(TPtr<CActor> actor, uint32 id=0);
    void ClearImpl();
    object CreateActorImpl(object components, TPtr<CActor> owner, object name, uint32 id);
    void DestroyActorImpl(TPtr<CActor> actor);
    void OnCreatedImpl();
    void OnDestroyedImpl();

    void BuildTileCollisionPolygons(shared_ptr<FTileLayer> layer, std::vector<FRectI>& out);
    object BuildTileCollisionPolygonsPy(shared_ptr<FTileLayer> layer);

    KH_DECL_GC()

public:
    static const uint8 PRE_TICK_GROUP = 0;
    static const uint8 TICK_GROUP = 1;
    static const uint8 POST_TICK_GROUP = 2;
    static const uint8 GROUP_COUNT = 3;

    CLevel(PyObject* self, object guid, unicode name, FSizeI const& size);
    ~CLevel();

    // Main
    void Clear();
    void Test(int num);
    void Tick(float deltaTime);
    void TickActors(float deltaTime);
    void AssertInited() const;
    void AddedToWorld(TPtr<CWorld> world);
    void RemovedFromWorld(TPtr<CWorld> world);

    // Actors
    uint32 CreateID();
    TPtr<CActor> CreateActor(object components=object(),
                             TPtr<CActor> owner=TPtr<CActor>(),
                             object name=object(),
                             uint32 id=0)
    {
        return call_method<TPtr<CActor>>(mSelf, "CreateActor", components, owner, name, id);
    }
    void DestroyActor(TPtr<CActor> actor) {
        call_method<void>(mSelf, "DestroyActor", actor);
    }
    bool RenameActor(TPtr<CActor> const& actor, object const& newname);
    void ClearActors();
    bool AddRenderable(TPtr<CActor> r);
    bool RemoveRenderable(TPtr<CActor> r);
    bool MoveActor(TPtr<CActor> const& actor,
                   FVector3F const& delta,
                   bool smooth=true);
    void StartGame();
    void StopGame();
    bool GetGameStarted() const { return mGameStarted; }
    TPtr<CGame> GetGame() const;

    // Geometry
    shared_ptr<FLevelLayer> CreateLayer(int Index);
    void SetTiles(int layer, tile_t tile);
    void SetTiles(int layer, FVector2I const& topos, shared_ptr<FTileData> tiledata);
    void SetTiles(int layer, FVector2I const& topos, FSizeI const& size, tile_t tile);
    void SetTiles(int layer, FVector2I const& topos, FSizeI const& size, object sequence);
    void SetTiles(int layer, FVector2I const& topos, FSizeI const& size, tile_t const* tiles);
    bool CollideTiles(FRectF const& dest, int layer);

    // Physics
    void AddCollidable(TPtr<ACollider> actor);
    void RemoveCollidable(TPtr<ACollider> actor);
    sint16 GetCollisionLayer() const { return mCollisionLayer; }
    void SetCollisionLayer(sint16 value) { mCollisionLayer = value; }

    // Events
    void OnCreated();
    void OnDestroyed();

    // Accessors
    bool GetInited() const;
    TPtr<CWorld> GetWorld() const;
    KH_DEF_GET(unicode const&, Name)
    KH_DEF_GET(object const&, NameHash)
    sint32 GetNameHashAsInt() const;
    KH_DEF_GET(object const&, GUID)
    KH_DEF_GET(const shared_ptr<core::FLogger>&, Logger)
    KH_DEF_GET(FSizeI const&, Size)
    KH_DEF_GET(FSizeI const&, TileGroupSize)
    KH_DEF_GET(ActorList const&, Actors)
    KH_DEF_GET(dict const&, ActorIDMap)
    KH_DEF_GET(RenderableList const&, Renderables)
    KH_DEF_GET(LayerList const&, Layers)
    int GetTileGroupCount() const { return mTileGroups.size(); }
    int GetActorCount() const { return mActors.size(); }
    TPtr<CActor> GetActorByTag(object name) const;
    object GetActorsByTagPy(object name) const;
    TPtr<CActor> GetActorByID(uint32 id) const;
    TPtr<CActor> GetActorByName(object name) const;
    TPtr<CActor> const& GetActorByIndex(int index) const { return mActors[index]; }

    shared_ptr<FTileGroup> const& GetTileGroup(uint index) const;
    shared_ptr<FTileGroup> const& GetTileGroup(FVector2I const& pos) const;
    shared_ptr<FTileGroup> const& GetTileGroupAt(FVector2I const& pos) const;
    shared_ptr<FTileGroup> const& GetTileGroupAt(FVector2F const& pos) const;
    FTileInfo const* GetTileInfo(int layer, FVector2I const& pos) const;

    int GetLayerCount() const { return mLayers.size(); }
    shared_ptr<FLevelLayer> GetLayer(uint index) const {
        return mLayers.at(index);
    }

    static const uint8 MAX_WIDTH = 0xFF;
    static const uint8 MAX_HEIGHT = 0xFF;

    static void SetPlayerControllerClass(object cls);
    static object GetActorClass();
    static void SetActorClass(object cls);

    friend void _InitLevelClasses();
    friend void _InitLevelBindings();
    friend void _ExitLevelClasses();
};

inline void CLevel::Clear() {
    call_method<void>(mSelf, "Clear");
}

inline void CLevel::AssertInited() const {
    if (!GetInited())
        throw RuntimeError("AssertInited() fail");
}

inline bool CLevel::GetInited() const {
    return !mGUID.is_none();
}

inline TPtr<CWorld> CLevel::GetWorld() const {
    return mWorld.Lock();
}

inline object CLevel::GetActorClass() {
    return msActorClass;
}

inline sint32 CLevel::GetNameHashAsInt() const {
    return static_cast<sint32>(PyInt_AS_LONG(mNameHash.ptr()));
}

inline void CLevel::OnCreated() {
    call_method<void>(mSelf, "OnCreated");
}

inline void CLevel::OnDestroyed() {
    call_method<void>(mSelf, "OnDestroyed");
}

inline shared_ptr<FTileGroup> const& CLevel::GetTileGroup(uint index) const { 
    return mTileGroups.at(index);
}

inline shared_ptr<FTileGroup> const& CLevel::GetTileGroup(FVector2I const& pos) const {
    return GetTileGroup((mSize.Width * pos.Y) + pos.X);
}

inline shared_ptr<FTileGroup> const& CLevel::GetTileGroupAt(FVector2I const& pos) const {
    return GetTileGroup(FVector2I(pos.X / mTileGroupSize.Width,
                                  pos.Y / mTileGroupSize.Height));
}

inline shared_ptr<FTileGroup> const& CLevel::GetTileGroupAt(FVector2F const& pos) const {
    return GetTileGroup(FVector2I(int(pos.X) / mTileGroupSize.Width,
                                  int(pos.Y) / mTileGroupSize.Height));
}

}} //namespace Kharlia

ENABLE_BACK_REFERENCE(kharlia::engine::CLevel)

#endif